本系列文章已出版實體書籍:
「你的地圖會說話?WebGIS 與 JavaScript 的情感交織」(博碩文化)
WebGIS啟蒙首選✖五家地圖API✖近百個程式範例✖實用簡易口訣✖學習難度分級✖補充ES6小知識
本系列文章為Leaflet Plugins介紹:
[8-1] 展點多到爆?那就試試看 Leaflet MarkerCluster吧!
上篇文章介紹了Leaflet Plugins的MarkerCluster群聚,
今天要來介紹另個也是很有趣的功能,熱區地圖。
空間分析(Spatial Analysis),是GIS中的核心概念之一。
最早最著名的空間分析,要從19世紀的英國倫敦說起。
西元1854年8月31日,倫敦霍亂爆發事件,在短短三天內造成127人死亡,至9月10日時,死亡人數已超過500人。在那個細菌尚未被人們發現的年代,人們不知道霍亂的傳染途徑為何。
當時的內科醫生John Snow懷疑霍亂是依據「水」為傳播途徑,於是他把倫敦街頭的公共水泵畫在地圖上,並把感染者從住家到公共水泵的路線及活動範圍圈出來,並用點子圖呈現,最終發現感染途徑為以公共水泵為中心向外擴散,成為空間分析最早具有「熱區」概念的地圖。
後來當局採用了醫生John Snow的說法,拆除了水泵閥,使得霍亂得以控制。
John Snow的貢獻也為流行病學及空間地理學有重要的開端。
今天使用的套件為heatmap.js,它是一個可以根據每個點的數值,計算過後,並用HTML5 canvas畫出熱區圖。給它座標後也可以套用在Google地圖或是Leaflet地圖上。
↓ 透過npm下載heatmap.js,不愛用npm的人可以去github或heatmap.js文件中下載
    npm install heatmap.js
↓ 下載完後,引入heatmap.js
    <script src="node_modules/heatmap.js/build/heatmap.min.js"></script>
↓建立一個用以存放heatmap canvas的div
    <div class="heatmap"></div>
↓ 起手式:h337.create()
        const heatmapInstance = h337.create({
            container: document.querySelector('.heatmap'),  //存放heatmap的div
        });
↓ 然後我們利用昨天寫的亂數產生器來產生範例點座標
        function random(min, max) {
            return Math.random() * (max - min) + min;
        }
↓ 相較於昨天,陣列中除了x座標及y座標外,熱區地圖還需要每個點的數值大小,以決定熱區的核密度,也就是點資料聚集的程度。
        let arr = [];
        function CreatePoint(count) {
            for (let i = 0; i < count; i++) {
                let x = Math.floor(random(0, window.innerWidth));
                let y = Math.floor(random(0, window.innerHeight));
                let value = Math.floor(random(0, 100));
                arr.push({ x: x, y: y, value: value });
            }
        }
HTML5 Canvas是以左上角為中心,x向右為正,y向下為正。因此x座標範圍設定從0~畫面寬度,y從0~畫面高度。
↓ 產生點
        CreatePoint(1500);
        const data = {  // 熱區繪製的資料格式
            max: 100,
            data: arr
        };
        console.log(data)
↓ console亂數產生的點
↓ 呼叫熱區圖
        heatmapInstance.setData(data);
↓ 結果
↓ 熱區圖可以設定核密度半徑、背景顏色、梯度等等,來打造自己的熱區圖吧!
        const heatmapInstance = h337.create({
            container: document.querySelector('.heatmap'),
            backgroundColor: 'rgba(0,0,0,.75)',
            //radius: 30,
            gradient: {
                // enter n keys between 0 and 1 here
                // for gradient color customization
                '.5': 'blue',
                '.8': 'red',
                '.95': 'yellow'
            },
            maxOpacity: .9,
            minOpacity: .3
        });
h337的熱區圖設定
↓ 結果
↓ 可以綁定dom元素的滑鼠事件,並且把滑鼠位置塞值給熱區圖物件。
        document.querySelector('.heatmap').onmousemove = function (e) {
            heatmapInstance.addData({
                x: e.layerX,
                y: e.layerY,
                value: 1
            });
        };
↓ 可以用滑鼠畫的熱區畫布
接下來我們要把熱區呈現在地圖上,結合Leaflet API。
↓ 引入heatmap.js外,還要引入leaflet-heatmap.js。
<script src="node_modules/heatmap.js/build/heatmap.min.js"></script>
<script src="node_modules/heatmap.js/plugins/leaflet-heatmap/leaflet-heatmap.js"></script>
↓ 存放地圖的div
    <div id="lmap"></div>
↓ 讓地圖滿板
    <style>
        html,
        body {
            padding: 0;
            margin: 0;
            height: 100%;
        }
        #lmap {
            height: 100%;
        }
    </style>
↓ 初始化地圖
        const LMap = L.map(document.getElementById('lmap'), {
            center: [23.5, 121],
            zoom: 7,
            crs: L.CRS.EPSG3857,
        });
        L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
            maxZoom: 18,
            id: 'mapbox.streets'
        }).addTo(LMap);
↓ 結果
↓ 這次我們亂數產生點,給定經度範圍及緯度範圍。
        let arr = [];
        function CreatePoint(count) {
            for (let i = 0; i < count; i++) {
                let longitude = random(120.5, 121.4);
                let latitude = random(23, 24.6);
                let value = Math.floor(random(0, 100));
                arr.push({ x: longitude, y: latitude, value: value });
            }
        }
        function random(min, max) {
            return Math.random() * (max - min) + min;
        }
        CreatePoint(100);
↓ 熱區地圖設定
        const option = {
            "scaleRadius": false,
            "radius": 50,
            "useLocalExtrema": true,
            latField: 'y',
            lngField: 'x',
            valueField: 'value',
            "maxOpacity": .5
        };
leaflet-heatmap.js使用HeatmapOverlay物件,比起heatmap.js的h337物件,多了三種設定:
↓ 呼叫熱區地圖
        const heatmapLayer = new HeatmapOverlay(option);
↓ 設定熱區地圖物件的資料
        const testData = {
            max: 100,
            data: arr
        };
        heatmapLayer.setData(testData);
↓ 把熱區地圖加入Leaflet圖台
        heatmapLayer.addTo(LMap);
核密度半徑
↓ radius 30
↓ 放大
↓ radius 50
↓ 放大
可以發現在小比例尺時,差距較不明顯,
中大比例尺時,半徑較大的,較能顯示核密度中心及其擴展性。
然而,半徑太大又會全部混雜在一起。
因此,根據不同的資料型態,要選擇不一樣的核密度半徑。
當前極值及定極值
useLocalExtrema:當前極值或定極值。如果為false,使用全域極值,也就是固定的最大最小極距;如果為true,則是使用當前最大數值及最小數值作為級距。
↓ useLocalExtrema: false 定極值(全域極值)
↓ useLocalExtrema: true 當前極值
可以發現使用當前極值,從當下的所有數值值中找出極大極小值,
較能看出它的顏色差異,可是級距變動下可能會混淆視聽,也要時常更動圖例。
定極值(全域極值)顏色差異較不明顯,視覺上熱區密度較不直觀,
但級距固定,較不易混淆。
今天從流行病學歷史講到空間地理學,再從熱區介紹到heatmap.js。
大家有沒有收穫呀?
明天再續Leaflet! ![]()